// @ts-nocheck
import { EventEmitter } from '../polyfill/events';
import { Readable as ReadableStream } from '../polyfill/stream';
import { inherits, inspect } from '../polyfill/util';
import process from '../polyfill/process';
import { Buffer } from '../polyfill/buffer';
import utf7 from '../dependencies/utf7';
import fs from '@ohos.file.fs';

var jsencoding; // lazy-loaded

var CH_LF = 10,
    LITPLACEHOLDER = String.fromCharCode(0),
    EMPTY_READCB = function (n) {
    },
    RE_INTEGER = /^\d+$/,
    RE_PRECEDING = /^(?:\* |A\d+ |\+ ?)/,
    RE_BODYLITERAL = /BODY\[(.*)\] \{(\d+)\}$/i,
    RE_BODYINLINEKEY = /^BODY\[(.*)\]$/i,
    RE_SEQNO = /^\* (\d+)/,
    RE_LISTCONTENT = /^\((.*)\)$/,
    RE_LITERAL = /\{(\d+)\}$/,
    RE_UNTAGGED = /^\* (?:(OK|NO|BAD|BYE|FLAGS|ID|LIST|XLIST|LSUB|SEARCH|STATUS|CAPABILITY|NAMESPACE|PREAUTH|SORT|THREAD|ESEARCH|QUOTA|QUOTAROOT)|(\d+) (EXPUNGE|FETCH|RECENT|EXISTS))(?:(?: \[([^\]]+)\])?(?: (.+))?)?$/i,
    RE_TAGGED = /^A(\d+) (OK|NO|BAD) ?(?:\[([^\]]+)\] )?(.*)$/i,
    RE_CONTINUE = /^\+(?: (?:\[([^\]]+)\] )?(.+))?$/i,
    RE_CRLF = /\r\n/g,
    RE_HDR = /^([^:]+):[ \t]?(.+)?$/,
    RE_ENCWORD = /=\?([^?*]*?)(?:\*.*?)?\?([qb])\?(.*?)\?=/gi,
    RE_ENCWORD_END = /=\?([^?*]*?)(?:\*.*?)?\?([qb])\?(.*?)\?=$/i,
    RE_ENCWORD_BEGIN = /^[ \t]=\?([^?*]*?)(?:\*.*?)?\?([qb])\?(.*?)\?=/i,
    RE_QENC = /(?:=([a-fA-F0-9]{2}))|_/g,
    RE_SEARCH_MODSEQ = /^(.+) \(MODSEQ (.+?)\)$/i,
    RE_LWS_ONLY = /^[ \t]*$/;

function Parser(stream, debug) {
    if (!(this instanceof Parser))
        return new Parser(stream, debug);

    EventEmitter.call(this);

    this._stream = undefined;
    this._body = undefined;
    this._literallen = 0;
    this._literals = [];
    this._buffer = '';
    this._ignoreReadable = false;
    this.debug = debug;
    this.cacheData = [];
    this.cacheNum = 0;
    this.lastSize = 0;
    var timerId = undefined;
    var self = this;
    this._cbReadable = function (value) {
        if (self._ignoreReadable)
            return;
        if (self._literallen > 0 && !self._body)
            self._tryread(self._literallen, value);
        else
            self._tryread(null, value);
    };

    this.setStream(stream);

    process.nextTick(this._cbReadable);
}

inherits(Parser, EventEmitter);

Parser.prototype.setStream = function (stream) {
    const ctx = this;
    if (this._stream) {
        this._stream.off('message');
    }
    if (/^v0\.8\./.test(process.version)) {
        this._stream = (new ReadableStream()).wrap(stream);

        // since Readable.wrap() proxies events, we need to remove at least the
        // proxied 'error' event since this can cause problems and Parser doesn't
        // care about such events
        stream._events.error.pop();
    } else
        this._stream = stream;

    this._stream.on('message', async (value) => {
        if (!value || !value.message) {
            return;
        }
        ctx.cacheData.push(value.message)
        if (!ctx.timerId) {
            let result = await new Promise(function (resolve, reject) {
                ctx.timerId = setInterval(function () {
                    let currentSize = ctx.cacheData.length;
                    if (currentSize === 0) {
                        return;
                    }
                    if (ctx.cacheNum >= 3) {
                        ctx.cacheNum = 0;
                        ctx.lastSize = 0;
                        resolve(ctx.cacheData);
                    }
                    if (currentSize != ctx.lastSize) {
                        ctx.lastSize = currentSize;
                        ctx.cacheNum = 0;
                        return;
                    }
                    if (currentSize === ctx.lastSize) {
                        ctx.cacheNum++;
                        return;
                    }
                }, 50)
            })
        }
        clearInterval(ctx.timerId)
        ctx.timerId = undefined;
        if (ctx.cacheData.length < 1) {
            return;
        }
        let totalLength = 0;
        for (let i = 0; i < ctx.cacheData.length; i++) {
            totalLength += ctx.cacheData[i].byteLength;
        }
        let newData = new Uint8Array(totalLength);
        let offset = 0;
        for (let i = 0; i < ctx.cacheData.length; i++) {
            let temp = new Uint8Array(ctx.cacheData[i])
            newData.set(temp, offset)
            offset += ctx.cacheData[i].byteLength;
        }
        ctx.cacheData = [];
        this._cbReadable(Buffer.from(newData.buffer))
        /* if (globalThis.context) {
             let filePath = globalThis.context.cacheDir + '/test.txt';
             console.log(`zdy---filePath--->${filePath}`)
             let file = fs.openSync(filePath, fs.OpenMode.CREATE | fs.OpenMode.READ_WRITE | fs.OpenMode.APPEND);
             fs.writeSync(file.fd, Buffer.from(value.message).toString() + '\r\n\r\n')
             fs.fsyncSync(file.fd);
             fs.closeSync(file);
         }*/

    });
};

Parser.prototype._tryread = function (n, value) {
    if (value) {
        this._parse(value);
    }
};

Parser.prototype._parse = function (data) {
    var i = 0, datalen = data.length, idxlf;

    if (this._literallen > 0) {
        if (this._body) {
            var body = this._body;
            if (datalen >= this._literallen) {
                var litlen = this._literallen;
                i = litlen;
                this._literallen = 0;
                this._body = undefined;
                body._read = EMPTY_READCB;
                if (datalen > litlen)
                    body.push(data.slice(0, litlen));
                else
                    body.push(data);
                body.push(null);
            } else {
                this._literallen -= datalen;
                var r = body.push(data);
                if (!r) {
                    body._read = this._cbReadable;
                    return;
                }
                i = datalen;
            }
        } else {
            if (datalen > this._literallen)
                this._literals.push(data.slice(0, this._literallen));
            else
                this._literals.push(data);
            i = this._literallen;
            this._literallen = 0;
        }
    }

    while (i < datalen) {
        idxlf = indexOfCh(data, datalen, i, CH_LF);
        if (this._literallen > 0) {
            if (idxlf === -1) {
                data = data.slice(i, datalen)
            } else {
                data = data.slice(idxlf + 1, datalen)
            }
        }
        if (idxlf === -1) {
            this._buffer += data.toString('utf8', i);
            break;
        } else {
            this._buffer += data.toString('utf8', i, idxlf);
            this._buffer = this._buffer.trim();
            i = idxlf + 1;

            this.debug && this.debug('<= ' + inspect(this._buffer));

            if (RE_PRECEDING.test(this._buffer)) {
                var firstChar = this._buffer[0];
                if (firstChar === '*')
                    this._resUntagged();
                else if (firstChar === 'A')
                    this._resTagged();
                else if (firstChar === '+')
                    this._resContinue();

                if (this._literallen > 0 && i < datalen) {
                    this._ignoreReadable = true;
                    // literal data included in this chunk -- put it back onto stream
                    // this._stream.unshift(data.slice(i));
                    data = data.slice(i);
                    this._ignoreReadable = false;
                    i = datalen;
                    if (!this._body) {
                        // check if unshifted contents satisfies non-body literal length
                        this._tryread(this._literallen, data);
                    }
                }
            } else {
                this.emit('other', this._buffer);
                this._buffer = '';
            }
        }
    }

    if (this._literallen === 0 || this._body){
        if (this._literallen === 0) {
            this._tryread(null, null);
        }else {
            this._tryread(null, data);
        }

    }

};

Parser.prototype._resTagged = function () {
    var m;
    if (m = RE_LITERAL.exec(this._buffer)) {
        // non-BODY literal -- buffer it
        this._buffer = this._buffer.replace(RE_LITERAL, LITPLACEHOLDER);
        this._literallen = parseInt(m[1], 10);
    } else if (m = RE_TAGGED.exec(this._buffer)) {
        this._buffer = '';
        this._literals = [];

        this.emit('tagged', {
            type: m[2].toLowerCase(),
            tagnum: parseInt(m[1], 10),
            textCode: (m[3] ? parseTextCode(m[3], this._literals) : m[3]),
            text: m[4]
        });
    } else
        this._buffer = '';
};

Parser.prototype._resUntagged = function () {
    var m;
    if (m = RE_BODYLITERAL.exec(this._buffer)) {
        // BODY literal -- stream it
        var which = m[1], size = parseInt(m[2], 10);
        this._literallen = size;
        this._body = new ReadableStream();
        this._body._readableState.sync = false;
        this._body._read = EMPTY_READCB;
        m = RE_SEQNO.exec(this._buffer);
        this._buffer = this._buffer.replace(RE_BODYLITERAL, '');
        this.emit('body', this._body, {
            seqno: parseInt(m[1], 10),
            which: which,
            size: size
        });
    } else if (m = RE_LITERAL.exec(this._buffer)) {
        // non-BODY literal -- buffer it
        this._buffer = this._buffer.replace(RE_LITERAL, LITPLACEHOLDER);
        this._literallen = parseInt(m[1], 10);
    } else if (m = RE_UNTAGGED.exec(this._buffer)) {
        this._buffer = '';
        // normal single line response

        // m[1] or m[3] = response type
        // if m[3] is set, m[2] = sequence number (for FETCH) or count
        // m[4] = response text code (optional)
        // m[5] = response text (optional)

        var type, num, textCode, val;
        if (m[2] !== undefined)
            num = parseInt(m[2], 10);
        if (m[4] !== undefined)
            textCode = parseTextCode(m[4], this._literals);

        type = (m[1] || m[3]).toLowerCase();

        if (type === 'flags'
        || type === 'search'
        || type === 'capability'
        || type === 'sort') {
            if (m[5]) {
                if (type === 'search' && RE_SEARCH_MODSEQ.test(m[5])) {
                    // CONDSTORE search response
                    var p = RE_SEARCH_MODSEQ.exec(m[5]);
                    val = {
                        results: p[1].split(' '),
                        modseq: p[2]
                    };
                } else {
                    if (m[5][0] === '(')
                        val = RE_LISTCONTENT.exec(m[5])[1].split(' ');
                    else
                        val = m[5].split(' ');

                    if (type === 'search' || type === 'sort')
                        val = val.map(function (v) {
                            return parseInt(v, 10);
                        });
                }
            } else
                val = [];
        } else if (type === 'thread') {
            if (m[5])
                val = parseExpr(m[5], this._literals);
            else
                val = [];
        } else if (type === 'list' || type === 'lsub' || type === 'xlist')
            val = parseBoxList(m[5], this._literals);
        else if (type === 'id')
            val = parseId(m[5], this._literals);
        else if (type === 'status')
            val = parseStatus(m[5], this._literals);
        else if (type === 'fetch')
            val = parseFetch.call(this, m[5], this._literals, num);
        else if (type === 'namespace')
            val = parseNamespaces(m[5], this._literals);
        else if (type === 'esearch')
            val = parseESearch(m[5], this._literals);
        else if (type === 'quota')
            val = parseQuota(m[5], this._literals);
        else if (type === 'quotaroot')
            val = parseQuotaRoot(m[5], this._literals);
        else
            val = m[5];

        this._literals = [];

        this.emit('untagged', {
            type: type,
            num: num,
            textCode: textCode,
            text: val
        });
    } else
        this._buffer = '';
};

Parser.prototype._resContinue = function () {
    var m = RE_CONTINUE.exec(this._buffer),
        textCode,
        text;

    this._buffer = '';

    if (!m)
        return;

    text = m[2];

    if (m[1] !== undefined)
        textCode = parseTextCode(m[1], this._literals);

    this.emit('continue', {
        textCode: textCode,
        text: text
    });
};

function indexOfCh(buffer, len, i, ch) {
    var r = -1;
    for (; i < len; ++i) {
        if (buffer[i] === ch) {
            r = i;
            break;
        }
    }
    return r;
}

function parseTextCode(text, literals) {
    var r = parseExpr(text, literals);
    if (r.length === 1)
        return r[0];
    else
        return { key: r[0], val: r.length === 2 ? r[1] : r.slice(1) };
}

function parseESearch(text, literals) {
    var r = parseExpr(text.toUpperCase().replace('UID', ''), literals),
        attrs = {};

    // RFC4731 unfortunately is lacking on documentation, so we're going to
    // assume that the response text always begins with (TAG "A123") and skip that
    // part ...

    for (var i = 1, len = r.length, key, val; i < len; i += 2) {
        key = r[i].toLowerCase();
        val = r[i + 1];
        if (key === 'all')
            val = val.toString().split(',');
        attrs[key] = val;
    }

    return attrs;
}

function parseId(text, literals) {
    var r = parseExpr(text, literals),
        id = {};
    if (r[0] === null)
        return null;
    for (var i = 0, len = r[0].length; i < len; i += 2)
        id[r[0][i].toLowerCase()] = r[0][i + 1];

    return id;
}

function parseQuota(text, literals) {
    var r = parseExpr(text, literals),
        resources = {};

    for (var i = 0, len = r[1].length; i < len; i += 3) {
        resources[r[1][i].toLowerCase()] = {
            usage: r[1][i + 1],
            limit: r[1][i + 2]
        };
    }

    return {
        root: r[0],
        resources: resources
    };
}

function parseQuotaRoot(text, literals) {
    var r = parseExpr(text, literals);

    return {
        roots: r.slice(1),
        mailbox: r[0]
    };
}

function parseBoxList(text, literals) {
    var r = parseExpr(text, literals);
    return {
        flags: r[0],
        delimiter: r[1],
        name: utf7.decode('' + r[2])
    };
}

function parseNamespaces(text, literals) {
    var r = parseExpr(text, literals), i, len, j, len2, ns, nsobj, namespaces, n;

    for (n = 0; n < 3; ++n) {
        if (r[n]) {
            namespaces = [];
            for (i = 0, len = r[n].length; i < len; ++i) {
                ns = r[n][i];
                nsobj = {
                    prefix: ns[0],
                    delimiter: ns[1],
                    extensions: undefined
                };
                if (ns.length > 2)
                    nsobj.extensions = {};
                for (j = 2, len2 = ns.length; j < len2; j += 2)
                    nsobj.extensions[ns[j]] = ns[j + 1];
                namespaces.push(nsobj);
            }
            r[n] = namespaces;
        }
    }

    return {
        personal: r[0],
        other: r[1],
        shared: r[2]
    };
}

function parseStatus(text, literals) {
    var r = parseExpr(text, literals), attrs = {};
    // r[1] is [KEY1, VAL1, KEY2, VAL2, .... KEYn, VALn]
    for (var i = 0, len = r[1].length; i < len; i += 2)
        attrs[r[1][i].toLowerCase()] = r[1][i + 1];
    return {
        name: utf7.decode('' + r[0]),
        attrs: attrs
    };
}

function parseFetch(text, literals, seqno) {
    var list = parseExpr(text, literals)[0], attrs = {}, m, body;
    // list is [KEY1, VAL1, KEY2, VAL2, .... KEYn, VALn]
    for (var i = 0, len = list.length, key, val; i < len; i += 2) {
        key = list[i].toLowerCase();
        val = list[i + 1];
        if (key === 'envelope')
            val = parseFetchEnvelope(val);
        else if (key === 'internaldate')
            val = new Date(val);
        else if (key === 'modseq') // always a list of one value
            val = '' + val[0];
        else if (key === 'body' || key === 'bodystructure')
            val = parseBodyStructure(val);
        else if (m = RE_BODYINLINEKEY.exec(list[i])) {
            // a body was sent as a non-literal
            val = Buffer.from('' + val);
            body = new ReadableStream();
            body._readableState.sync = false;
            body._read = EMPTY_READCB;
            this.emit('body', body, {
                seqno: seqno,
                which: m[1],
                size: val.length
            });
            body.push(val);
            body.push(null);
            continue;
        }
        attrs[key] = val;
    }
    return attrs;
}

function parseBodyStructure(cur, literals, prefix, partID) {
    var ret = [], i, len;
    if (prefix === undefined) {
        var result = (Array.isArray(cur) ? cur : parseExpr(cur, literals));
        if (result.length)
            ret = parseBodyStructure(result, literals, '', 1);
    } else {
        var part, partLen = cur.length, next;
        if (Array.isArray(cur[0])) { // multipart
            next = -1;
            while (Array.isArray(cur[++next])) {
                ret.push(parseBodyStructure(cur[next],
                    literals,
                    prefix + (prefix !== '' ? '.' : '')
                    + (partID++).toString(), 1));
            }
            part = { type: cur[next++].toLowerCase() };
            if (partLen > next) {
                if (Array.isArray(cur[next])) {
                    part.params = {};
                    for (i = 0, len = cur[next].length; i < len; i += 2)
                        part.params[cur[next][i].toLowerCase()] = cur[next][i + 1];
                } else
                    part.params = cur[next];
                ++next;
            }
        } else { // single part
            next = 7;
            if (typeof cur[1] === 'string') {
                part = {
                    // the path identifier for this part, useful for fetching specific
                    // parts of a message
                    partID: (prefix !== '' ? prefix : '1'),

                    // required fields as per RFC 3501 -- null or otherwise
                    type: cur[0].toLowerCase(),
                    subtype: cur[1].toLowerCase(),
                    params: null,
                    id: cur[3],
                    description: cur[4],
                    encoding: cur[5],
                    size: cur[6]
                };
            } else {
                // type information for malformed multipart body
                part = { type: cur[0] ? cur[0].toLowerCase() : null, params: null };
                cur.splice(1, 0, null);
                ++partLen;
                next = 2;
            }
            if (Array.isArray(cur[2])) {
                part.params = {};
                for (i = 0, len = cur[2].length; i < len; i += 2)
                    part.params[cur[2][i].toLowerCase()] = cur[2][i + 1];
                if (cur[1] === null)
                    ++next;
            }
            if (part.type === 'message' && part.subtype === 'rfc822') {
                // envelope
                if (partLen > next && Array.isArray(cur[next]))
                    part.envelope = parseFetchEnvelope(cur[next]);
                else
                    part.envelope = null;
                ++next;

                // body
                if (partLen > next && Array.isArray(cur[next]))
                    part.body = parseBodyStructure(cur[next], literals, prefix, 1);
                else
                    part.body = null;
                ++next;
            }
            if ((part.type === 'text'
            || (part.type === 'message' && part.subtype === 'rfc822'))
            && partLen > next)
                part.lines = cur[next++];
            if (typeof cur[1] === 'string' && partLen > next)
                part.md5 = cur[next++];
        }
        // add any extra fields that may or may not be omitted entirely
        parseStructExtra(part, partLen, cur, next);
        ret.unshift(part);
    }
    return ret;
}

function parseStructExtra(part, partLen, cur, next) {
    if (partLen > next) {
        // disposition
        // null or a special k/v list with these kinds of values:
        // e.g.: ['Foo', null]
        //       ['Foo', ['Bar', 'Baz']]
        //       ['Foo', ['Bar', 'Baz', 'Bam', 'Pow']]
        var disposition = { type: null, params: null };
        if (Array.isArray(cur[next])) {
            disposition.type = cur[next][0];
            if (Array.isArray(cur[next][1])) {
                disposition.params = {};
                for (var i = 0, len = cur[next][1].length, key; i < len; i += 2) {
                    key = cur[next][1][i].toLowerCase();
                    disposition.params[key] = cur[next][1][i + 1];
                }
            }
        } else if (cur[next] !== null)
            disposition.type = cur[next];

        if (disposition.type === null)
            part.disposition = null;
        else
            part.disposition = disposition;

        ++next;
    }
    if (partLen > next) {
        // language can be a string or a list of one or more strings, so let's
        // make this more consistent ...
        if (cur[next] !== null)
            part.language = (Array.isArray(cur[next]) ? cur[next] : [cur[next]]);
        else
            part.language = null;
        ++next;
    }
    if (partLen > next)
        part.location = cur[next++];
    if (partLen > next) {
        // extension stuff introduced by later RFCs
        // this can really be any value: a string, number, or (un)nested list
        // let's not parse it for now ...
        part.extensions = cur[next];
    }
}

function parseFetchEnvelope(list) {
    return {
        date: new Date(list[0]),
        subject: decodeWords(list[1]),
        from: parseEnvelopeAddresses(list[2]),
        sender: parseEnvelopeAddresses(list[3]),
        replyTo: parseEnvelopeAddresses(list[4]),
        to: parseEnvelopeAddresses(list[5]),
        cc: parseEnvelopeAddresses(list[6]),
        bcc: parseEnvelopeAddresses(list[7]),
        inReplyTo: list[8],
        messageId: list[9]
    };
}

function parseEnvelopeAddresses(list) {
    var addresses = null;
    if (Array.isArray(list)) {
        addresses = [];
        var inGroup = false, curGroup;
        for (var i = 0, len = list.length, addr; i < len; ++i) {
            addr = list[i];
            if (addr[2] === null) { // end of group addresses
                inGroup = false;
                if (curGroup) {
                    addresses.push(curGroup);
                    curGroup = undefined;
                }
            } else if (addr[3] === null) { // start of group addresses
                inGroup = true;
                curGroup = {
                    group: addr[2],
                    addresses: []
                };
            } else { // regular user address
                var info = {
                    name: decodeWords(addr[0]),
                    mailbox: addr[2],
                    host: addr[3]
                };
                if (inGroup)
                    curGroup.addresses.push(info);
                else if (!inGroup)
                    addresses.push(info);
            }
            list[i] = addr;
        }
        if (inGroup) {
            // no end of group found, assume implicit end
            addresses.push(curGroup);
        }
    }
    return addresses;
}

function parseExpr(o, literals, result, start, useBrackets) {
    start = start || 0;
    var inQuote = false,
        lastPos = start - 1,
        isTop = false,
        isBody = false,
        escaping = false,
        val;

    if (useBrackets === undefined)
        useBrackets = true;
    if (!result)
        result = [];
    if (typeof o === 'string') {
        o = { str: o };
        isTop = true;
    }
    for (var i = start, len = o.str.length; i < len; ++i) {
        if (!inQuote) {
            if (isBody) {
                if (o.str[i] === ']') {
                    val = convStr(o.str.substring(lastPos + 1, i + 1), literals);
                    result.push(val);
                    lastPos = i;
                    isBody = false;
                }
            } else if (o.str[i] === '"')
                inQuote = true;
            else if (o.str[i] === ' '
            || o.str[i] === ')'
            || (useBrackets && o.str[i] === ']')) {
                if (i - (lastPos + 1) > 0) {
                    val = convStr(o.str.substring(lastPos + 1, i), literals);
                    result.push(val);
                }
                if ((o.str[i] === ')' || (useBrackets && o.str[i] === ']')) && !isTop)
                    return i;
                lastPos = i;
            } else if ((o.str[i] === '(' || (useBrackets && o.str[i] === '['))) {
                if (o.str[i] === '['
                && i - 4 >= start
                && o.str.substring(i - 4, i).toUpperCase() === 'BODY') {
                    isBody = true;
                    lastPos = i - 5;
                } else {
                    var innerResult = [];
                    i = parseExpr(o, literals, innerResult, i + 1, useBrackets);
                    lastPos = i;
                    result.push(innerResult);
                }
            }
        } else if (o.str[i] === '\\')
            escaping = !escaping;
        else if (o.str[i] === '"') {
            if (!escaping)
                inQuote = false;
            escaping = false;
        }
        if (i + 1 === len && len - (lastPos + 1) > 0)
            result.push(convStr(o.str.substring(lastPos + 1), literals));
    }
    return (isTop ? result : start);
}

function convStr(str, literals) {
    if (str[0] === '"') {
        str = str.substring(1, str.length - 1);
        var newstr = '', isEscaping = false, p = 0;
        for (var i = 0, len = str.length; i < len; ++i) {
            if (str[i] === '\\') {
                if (!isEscaping)
                    isEscaping = true;
                else {
                    isEscaping = false;
                    newstr += str.substring(p, i - 1);
                    p = i;
                }
            } else if (str[i] === '"') {
                if (isEscaping) {
                    isEscaping = false;
                    newstr += str.substring(p, i - 1);
                    p = i;
                }
            }
        }
        if (p === 0)
            return str;
        else {
            newstr += str.substring(p);
            return newstr;
        }
    } else if (str === 'NIL')
        return null;
    else if (RE_INTEGER.test(str)) {
        // some IMAP extensions utilize large (64-bit) integers, which JavaScript
        // can't handle natively, so we'll just keep it as a string if it's too big
        var val = parseInt(str, 10);
        return (val.toString() === str ? val : str);
    } else if (literals && literals.length && str === LITPLACEHOLDER) {
        var l = literals.shift();
        if (Buffer.isBuffer(l))
            l = l.toString('utf8');
        return l;
    }

    return str;
}

function repeat(chr, len) {
    var s = '';
    for (var i = 0; i < len; ++i)
        s += chr;
    return s;
}

function decodeBytes(buf, encoding, offset, mlen, pendoffset, state, nextBuf) {
    if (!jsencoding)
        jsencoding = require('./deps/encoding/encoding');
    if (jsencoding.encodingExists(encoding)) {
        if (state.buffer !== undefined) {
            if (state.encoding === encoding && state.consecutive) {
                // concatenate buffer + current bytes in hopes of finally having
                // something that's decodable
                var newbuf = Buffer.allocUnsafe(state.buffer.length + buf.length);
                state.buffer.copy(newbuf, 0);
                buf.copy(newbuf, state.buffer.length);
                buf = newbuf;
            } else {
                // either:
                //   - the current encoded word is not separated by the previous partial
                //     encoded word by linear whitespace, OR
                //   - the current encoded word and the previous partial encoded word
                //     use different encodings
                state.buffer = state.encoding = undefined;
                state.curReplace = undefined;
            }
        }
        var ret, isPartial = false;
        if (state.remainder !== undefined) {
            // use cached remainder from the previous lookahead
            ret = state.remainder;
            state.remainder = undefined;
        } else {
            try {
                ret = jsencoding.TextDecoder(encoding).decode(buf);
            } catch (e) {
                if (e.message.indexOf('Seeking') === 0)
                    isPartial = true;
            }
        }
        if (!isPartial && nextBuf) {
            // try to decode a lookahead buffer (current buffer + next buffer)
            // and see if it starts with the decoded value of the current buffer.
            // if not, the current buffer is partial
            var lookahead, lookaheadBuf =
            Buffer.allocUnsafe(buf.length + nextBuf.length);
            buf.copy(lookaheadBuf);
            nextBuf.copy(lookaheadBuf, buf.length);
            try {
                lookahead = jsencoding.TextDecoder(encoding).decode(lookaheadBuf);
            } catch (e) {
                // cannot decode the lookahead, do nothing
            }
            if (lookahead !== undefined) {
                if (lookahead.indexOf(ret) === 0) {
                    // the current buffer is whole, cache the lookahead's remainder
                    state.remainder = lookahead.substring(ret.length);
                } else {
                    isPartial = true;
                    ret = undefined;
                }
            }
        }
        if (ret !== undefined) {
            if (state.curReplace) {
                // we have some previous partials which were finally "satisfied" by the
                // current encoded word, so replace from the beginning of the first
                // partial to the end of the current encoded word
                state.replaces.push({
                    fromOffset: state.curReplace[0].fromOffset,
                    toOffset: offset + mlen,
                    val: ret
                });
                state.replaces.splice(state.replaces.indexOf(state.curReplace), 1);
                state.curReplace = undefined;
            } else {
                // normal case where there are no previous partials and we successfully
                // decoded a single encoded word
                state.replaces.push({
                    // we ignore linear whitespace between consecutive encoded words
                    fromOffset: state.consecutive ? pendoffset : offset,
                    toOffset: offset + mlen,
                    val: ret
                });
            }
            state.buffer = state.encoding = undefined;
            return;
        } else if (isPartial) {
            // RFC2047 says that each decoded encoded word "MUST represent an integral
            // number of characters. A multi-octet character may not be split across
            // adjacent encoded-words." However, some MUAs appear to go against this,
            // so we join broken encoded words separated by linear white space until
            // we can successfully decode or we see a change in encoding
            state.encoding = encoding;
            state.buffer = buf;
            if (!state.curReplace)
                state.replaces.push(state.curReplace = []);
            state.curReplace.push({
                fromOffset: offset,
                toOffset: offset + mlen,
                // the value we replace this encoded word with if it doesn't end up
                // becoming part of a successful decode
                val: repeat('\uFFFD', buf.length)
            });
            return;
        }
    }
    // in case of unexpected error or unsupported encoding, just substitute the
    // raw bytes
    state.replaces.push({
        fromOffset: offset,
        toOffset: offset + mlen,
        val: buf.toString('latin1')
    });
}

function qEncReplacer(match, byte) {
    if (match === '_')
        return ' ';
    else
        return String.fromCharCode(parseInt(byte, 16));
}

function decodeWords(str, state) {
    var pendoffset = -1;

    if (!state) {
        state = {
            buffer: undefined,
            encoding: undefined,
            consecutive: false,
            replaces: undefined,
            curReplace: undefined,
            remainder: undefined
        };
    }

    state.replaces = [];

    var bytes, m, next, i, j, leni, lenj, seq, replaces = [], lastReplace = {};

    // join consecutive q-encoded words that have the same charset first
    while (m = RE_ENCWORD.exec(str)) {
        seq = {
            consecutive: (pendoffset > -1
                ? RE_LWS_ONLY.test(str.substring(pendoffset, m.index))
                : false),
            charset: m[1].toLowerCase(),
            encoding: m[2].toLowerCase(),
            chunk: m[3],
            index: m.index,
            length: m[0].length,
            pendoffset: pendoffset,
            buf: undefined
        };
        lastReplace = replaces.length && replaces[replaces.length - 1];
        if (seq.consecutive
        && seq.charset === lastReplace.charset
        && seq.encoding === lastReplace.encoding
        && seq.encoding === 'q') {
            lastReplace.length += seq.length + seq.index - pendoffset;
            lastReplace.chunk += seq.chunk;
        } else {
            replaces.push(seq);
            lastReplace = seq;
        }
        pendoffset = m.index + m[0].length;
    }

    // generate replacement substrings and their positions
    for (i = 0, leni = replaces.length; i < leni; ++i) {
        m = replaces[i];
        state.consecutive = m.consecutive;
        if (m.encoding === 'q') {
            // q-encoding, similar to quoted-printable
            bytes = Buffer.from(m.chunk.replace(RE_QENC, qEncReplacer), 'latin1');
            next = undefined;
        } else {
            // base64
            bytes = m.buf || Buffer.from(m.chunk, 'base64');
            next = replaces[i + 1];
            if (next && next.consecutive && next.encoding === m.encoding
            && next.charset === m.charset) {
                // we use the next base64 chunk, if any, to determine the integrity
                // of the current chunk
                next.buf = Buffer.from(next.chunk, 'base64');
            }
        }
        decodeBytes(bytes, m.charset, m.index, m.length, m.pendoffset, state,
            next && next.buf);
    }

    // perform the actual replacements
    for (i = state.replaces.length - 1; i >= 0; --i) {
        seq = state.replaces[i];
        if (Array.isArray(seq)) {
            for (j = 0, lenj = seq.length; j < lenj; ++j) {
                str = str.substring(0, seq[j].fromOffset)
                + seq[j].val
                + str.substring(seq[j].toOffset);
            }
        } else {
            str = str.substring(0, seq.fromOffset)
            + seq.val
            + str.substring(seq.toOffset);
        }
    }

    return str;
}

function parseHeader(str, noDecode) {
    var lines = str.split(RE_CRLF),
        len = lines.length,
        header = {},
        state = {
            buffer: undefined,
            encoding: undefined,
            consecutive: false,
            replaces: undefined,
            curReplace: undefined,
            remainder: undefined
        },
        m, h, i, val;

    for (i = 0; i < len; ++i) {
        if (lines[i].length === 0)
            break; // empty line separates message's header and body
        if (lines[i][0] === '\t' || lines[i][0] === ' ') {
            if (!Array.isArray(header[h]))
                continue; // ignore invalid first line
            // folded header content
            val = lines[i];
            if (!noDecode) {
                if (RE_ENCWORD_END.test(lines[i - 1])
                && RE_ENCWORD_BEGIN.test(val)) {
                    // RFC2047 says to *ignore* leading whitespace in folded header values
                    // for adjacent encoded-words ...
                    val = val.substring(1);
                }
            }
            header[h][header[h].length - 1] += val;
        } else {
            m = RE_HDR.exec(lines[i]);
            if (m) {
                h = m[1].toLowerCase().trim();
                if (m[2]) {
                    if (header[h] === undefined)
                        header[h] = [m[2]];
                    else
                        header[h].push(m[2]);
                } else
                    header[h] = [''];
            } else
                break;
        }
    }
    if (!noDecode) {
        var hvs;
        for (h in header) {
            hvs = header[h];
            for (i = 0, len = header[h].length; i < len; ++i)
                hvs[i] = decodeWords(hvs[i], state);
        }
    }

    return header;
}
;

export { Parser, parseExpr, parseEnvelopeAddresses, parseBodyStructure, parseHeader }